﻿using System;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using System.Reflection;

namespace Postnuclear.Dependency.Tree
  {
  /// <summary>
  /// Adapts object event to <see cref="IDependencyTreeElement{THolder}"/> interface.
  /// </summary>
  /// <typeparam name="THolder">The type of the holder.</typeparam>
  internal sealed class EventAdapter<THolder>
    : NotifyValueChanged, IDependencyTreeElement<THolder>
    where THolder : class
    {
    #region Public

    // Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="EventAdapter{THolder}"/> class.
    /// </summary>
    /// <param name="member">The member which should be observed.</param>
    public EventAdapter(MemberInfo member)
      {
      Contract.Requires<ArgumentNullException>(member != null);
      Contract.Requires<ArgumentException>(member.DeclaringType.IsAssignableFrom(typeof(THolder)));

      EventInfo eventInfo = InitEvent(member, RaiseValueChanged, out Handler);

      if (eventInfo == null)
        {
        AddHandler = (_, __) =>
        {
        };

        RemoveHandler = (_, __) =>
        {
        };
        }
      else
        {
        MethodInfo addHandler = eventInfo.GetAddMethod(true);
        AddHandler = (holder, handler) => addHandler.Invoke(holder, new object[] { Handler });

        MethodInfo removeHandler = eventInfo.GetRemoveMethod(true);
        RemoveHandler = (holder, handler) => removeHandler.Invoke(holder, new object[] { Handler });
        }
      }

    // Methods

    /// <summary>
    /// Updates the dependency holder instance.
    /// </summary>
    /// <param name="holder">The holder.</param>
    public void UpdateHolderValue(THolder holder)
      {
      if (Holder == holder)
        {
        return;
        }

      if (IsActive)
        {
        Detach();
        }

      Holder = holder;

      if (IsActive)
        {
        Attach();
        }
      }

    #endregion

    #region Protected

    // Methods

    /// <summary>
    /// Called when when activation state is changed.
    /// </summary>
    protected override void OnIsActiveChanged()
      {
      if (IsActive)
        {
        Attach();
        }
      else
        {
        Detach();
        }
      }

    #endregion

    #region Private

    // Properties

    private WeakReference<THolder> Holder
      {
      get;
      set;
      }

    // Methods

    private static EventInfo InitEvent(MemberInfo member, EventHandler<EventArgs> handler, out Delegate eventHandler)
      {
      eventHandler = null;
      Type holder = member.ReflectedType;

      // You can also include in the dependencies the private property of the instance which defines the dependency. 
      // Thus you need to know about the private members as well.
      EventInfo eventInfo = FindEvent(holder, member.Name + "Changed");
      LambdaExpression raiseEventExpression = null;

      if (eventInfo != null)
        {
        // There is the specific event for this property
        raiseEventExpression = (Expression<EventHandler<EventArgs>>)((sender, e) => handler(sender, e));
        }
      else if (typeof(INotifyCollectionChanged).IsAssignableFrom(holder))
        {
        // The holder is the observable collection. Once the collection is changed the property should be reevaluated
        eventInfo = FindEvent(holder, "CollectionChanged");
        Contract.Assert(eventInfo != null, "The event should be there - something is wrong.");

        raiseEventExpression = (Expression<NotifyCollectionChangedEventHandler>)((sender, e) => handler(sender, e));
        }
      else if (typeof(INotifyPropertyChanged).IsAssignableFrom(holder))
        {
        // The holder implements the INotifyPropertyChanged interface.
        // Event handler should check if the property name in event arguments is the same as the name of tracked property.
        // If it is the property value is reevaluated.
        eventInfo = FindEvent(holder, "PropertyChanged");
        Contract.Assert(eventInfo != null, "The event should be there - something is wrong.");

        Action<object, PropertyChangedEventArgs> notifypropertyChangedHandler = (sender, e) =>
        {
          if (e.PropertyName == member.Name)
            {
            handler(sender, e);
            }
        };

        raiseEventExpression = (Expression<PropertyChangedEventHandler>)((sender, e) => notifypropertyChangedHandler(sender, e));
        }

      if (eventInfo != null)
        {
        // Event handler creates strong reference to dependency tracker (this) thus it's garbage collected only with tracking target or after detaching
        eventHandler = Expression.Lambda(
          eventInfo.EventHandlerType,
          raiseEventExpression.Body,
          raiseEventExpression.Parameters).Compile();
        }

      // We have no predefined mechanism to track the property
      return eventInfo;
      }

#if SILVERLIGHT
    private static EventInfo FindEvent(Type type, string name, BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
#else
    private static EventInfo FindEvent(Type type, string name, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static)
#endif
      {
      if (!type.IsInterface)
        {
        return type.GetEvent(name, flags);
        }

      EventInfo eventInfo = type.GetEvent(name, flags);

      if (eventInfo != null)
        {
        return eventInfo;
        }

      foreach (Type baseInterface in type.GetInterfaces())
        {
        eventInfo = FindEvent(baseInterface, name, flags);

        if (eventInfo != null)
          {
          return eventInfo;
          }
        }

      return null;
      }

    private void Attach()
      {
      if (Holder == null)
        {
        return;
        }

      THolder holder;
      if (Holder.TryGetTarget(out holder))
        {
        AddHandler(holder, Handler);
        }
      }

    private void Detach()
      {
      if (Holder == null)
        {
        return;
        }

      THolder holder;
      if (Holder.TryGetTarget(out holder))
        {
        RemoveHandler(holder, Handler);
        }
      }

    // Fields

    private readonly Delegate Handler;
    private readonly Action<THolder, Delegate> AddHandler;
    private readonly Action<THolder, Delegate> RemoveHandler;

    #endregion
    }
  }